// Record the item datatable passed to us.
// Redraw is the only function that uses this directly.
var greenMachineInputTable;

var subjectName = {10: 'English', 20: 'Math', 30: 'Science'};

function greenMachine(inputTable) {
    // Record the input table for redraw.
    greenMachineInputTable = inputTable;

    // Set menu selections to values that occur in this table.
    var c = inputTable.getTableProperty('c');  // column index lookup
    var grades = inputTable.getDistinctValues(c.Grade);
    var menu = $('select#grade_select').empty();
    var i;
    for (i = 0; i < grades.length; i++)
        menu.append('<option>' + grades[i] + '</option>');
    $('option:last', menu).attr('selected', true);
    var subjectIds = inputTable.getDistinctValues(c.Subject_ID);
    menu = $('select#subject_select').empty();
    for (i = 0; i < subjectIds.length; i++)
        menu.append('<option value="' + subjectIds[i] + '">' + subjectName[subjectIds[i]] + '</option>');
    var years = inputTable.getDistinctValues(c.Year);
    menu = $('select#year_select').empty();
    for (i = 0; i < years.length; i++)
        menu.append('<option>' + years[i] + '</option>');
    $('option:last', menu).attr('selected', true);

    redraw(true);
}

function redraw(erase) {
    // Always remove the 'no records' announcement.
    $('#no_records').remove();
    var c = greenMachineInputTable.getTableProperty('c');  // column index lookup
    // Filter datatable.
    var view = filter(greenMachineInputTable);
    // Any records?
    if (view.getNumberOfRows() == 0) {
        // No records, clear the screen.
        $('div[id^=vis_div]').empty();
        $('div#control_div').append('<p id="no_records" style="font-weight: bold">No records in the current collection match those criteria.</p>');
    }
    else {
        // Draw.
        if (erase) $('div[id^=vis_div]').empty();
        var w = $(window).width() - 30,
            h = $(window).height() - 30;
        var calc = selected('subtract_state') ? 'subtractState' : 'normal';
        if (selected('state')) {
            icicle(view, 'vis_div_left', w / 2, h, 'left', calc);
            icicle(view, 'vis_div_right', w / 2, h, 'right', 'state');
        } else {
            icicle(view, 'vis_div_left', w, h, 'left', calc);
        }
    }
}

// Filter a datatable according to UI control selections, returning a dataview.
function filter(table) {
    var c = table.getTableProperty('c');  // column index lookup
    var view = new google.visualization.DataView(table);
    view.setRows(table.getFilteredRows([
            {column: c.Grade, value: Number(selected('grade'))},
            {column: c.Year, value: Number(selected('year'))},
            {column: c['Subject_ID'], value: Number(selected('subject'))}
        ]));
    var selectedTypes = [];
    $('input[name=include_types]:checked').each(function() { selectedTypes.push($(this).val()) });
    if (selectedTypes.length != 4) {
        // Filter for selected item types.
        var include = [],
            nRows = view.getNumberOfRows();
        function rightType(type) {
            for (var t = 0; t < selectedTypes.length; t++) {
                if (type == selectedTypes[t])
                    return true;
            }
            return false;
        }
        for (var r = 0; r < nRows; r++) {
            if (rightType(view.getValue(r, c.Question_Type)))
                include.push(view.getTableRowIndex(r))
        }
        view.setRows(include);
    }
    return view;
}

// Fetch selections.
function selected(item) {
    switch (item) {
        case 'subjectName':
            return $('select#subject_select option:selected').text();
            break;
        case 'sort':
            return $('input[name=sort_order]:checked').val();
            break;
        case 'state':
            return $('input[name=cf_state]:checked').length > 0;
            break;
        case 'subtract_state':
            return $('input[name=subtract_state]:checked').length > 0;
            break;
        default:
            return $('select#' + item + '_select option:selected').val();
            break;
    }
}

// Make an icicle.
function icicle(table, divId, w, h, orient, what) {
    if (!what) what = 'normal';
    var c = table.getTableProperty('c');  // column index lookup
    var vis = new pv.Panel()
        .canvas(divId)
        .width(w)
        .height(h)
        ;
    var ice = vis.add(pv.Layout.Partition.Fill)
        .nodes(sortTree(dom(table, what), orient))
        .size(function(n) { return n.nodeValue.Possible })
        .orient(orient)
        ;
    var bar = ice.node.add(pv.Bar)
        .fillStyle(percentScoreColor)
        .strokeStyle('silver')
        .title(
            function(n) { 
                if (isLeaf(n)) {
                    text = table.getValue(n.nodeValue.row, c['Question_Type']);
                    text += ' question ' + n.nodeName;
                } else {
                    text = n.nodeName;
                }
                return text + ':  average ' + avePointsString(n) + ' out of ' +
                    n.nodeValue.itemPossible + 
                    ' point' + (n.nodeValue.itemPossible > 1 ? 's' : '') + 
                    '   ' + percentScoreString(n);
            })
        .cursor(function(n) { return isLeaf(n) ? 'pointer' : 'auto' })
        ;

    ice.label.add(pv.Label)
        .visible(function(n) { return !isRoot(n) })
        .text(function(n) { return n.nodeName })
        .textStyle(cellForegroundColor)
        ;

    // Capture click events over item blocks, to display question texts.
    bar.add(pv.Panel)
        .visible(isLeaf)
        .fillStyle(null)
        .strokeStyle(null)
        .events('all')
        .event('click', displayItem)
        ;

    vis.render();
}

var levels = ['test', 'strand', 'standard', 'item'];
var depth = {test: 0, strand: 1, standard: 2, item: 3};

// Return the nodes of a DOM tree for the datatable.
// Leaf (item) node values have attributes {Points, Possible, itemPossible, row}.
function dom(table, what) {
    var c = table.getTableProperty('c');  // column index lookup
    var nest = pv.nest(pv.range(0, table.getNumberOfRows()))
        .key(function(row) { return table.getValue(row, c.Strand) })
        .key(function(row) { return table.getValue(row, c.Standard) })
        .key(function(row) { return table.getValue(row, c.Item) })
        .rollup(
            function(rowSet) { 
                var row = rowSet[0];  // should be only one (per item)
                var itemPossible = parseInt(table.getValue(row, c['Correct_Response']));
                if (isNaN(itemPossible)) itemPossible = 1;
                return {
                    Points: table.getValue(row, what == 'state' ? c.State_Points : c.Points),
                    Possible: table.getValue(row, what == 'state' ? c.State_Possible : c.Possible),
                    statePoints: table.getValue(row, c.State_Points),
                    statePossible: table.getValue(row, c.State_Possible),
                    itemPossible: itemPossible,
                    table: table,
                    row: row,
                    calc: what
                    };
            });
    var d = pv.dom(nest)
        .leaf(function(o) { return o.Points !== undefined })
        .nodes();
    // Name the root node.
    var root = domRoot(d[0]);
    root.nodeName = 'Grade ' + selected('grade') + ' ' + selected('subjectName') + ' ' + selected('year');
    // Traverse the tree. Add up point values at each level. Record level names.
    domRoot(d[0]).visitAfter(
        function(node, level) {
            if (node.nodeValue === undefined) {
                node.nodeValue = {Points: 0, Possible: 0, itemPossible: 0, statePoints: 0, statePossible: 0, 
                    table: node.firstChild.nodeValue.table,
                    row: node.firstChild.nodeValue.row,
                    calc: node.firstChild.nodeValue.calc};
                for (var child = node.firstChild; child; child = child.nextSibling)
                    addNodeValue(node, child);
            }
            node.nodeValue.level = levels[level];
        });
    return d;
}

// Compute performance for a node.
// Return a value between 0 and 1 for points or -1 to 1 for district minus state.
function nodePerformance(node) {
    switch (node.nodeValue.calc) {
        case 'subtractState':
            return node.nodeValue.Points / node.nodeValue.Possible - node.nodeValue.statePoints / node.nodeValue.statePossible;
            break;
        default:
            return node.nodeValue.Points / node.nodeValue.Possible;
            break;
    }
}

// Compare two sibling nodes for ordering.
function compNodesStandard(a, b) {
    // Standard order is by strand ID, standard ID, item #.
    var table = a.nodeValue.table;
    var c = table.getTableProperty('c');  // column index lookup
    var aRow = a.nodeValue.row,
        bRow = b.nodeValue.row;
    switch (a.nodeValue.level) {
        case 'strand':
            return table.getValue(aRow, c.Strand_ID) - table.getValue(bRow, c.Strand_ID);
        case 'standard':
            return table.getValue(aRow, c.Standard_ID) - table.getValue(bRow, c.Standard_ID);
        case 'item':
            return Number(a.nodeName) - Number(b.nodeName);
            break;
    }
}
function compNodesAlphaNum(a, b) {
    // Alphanumeric sort; now unused.
    switch (a.nodeValue.level) {
        case 'strand':
        case 'standard':
            if (a.nodeName < b.nodeName)
                return -1;
            else if (a.nodeName > b.nodeName)
                return 1;
            else
                return 0;
            break;
        case 'item':
            return Number(a.nodeName) - Number(b.nodeName);
            break;
    }
}
function compNodesPerformance(a, b) {
    // Sort by performance.
    return nodePerformance(a) - nodePerformance(b);
}

// Sort the DOM tree.
function sortTree(nodes, orient) {
    switch (orient) {
        case 'right':
            function compStandard(a, b) { return compNodesStandard(b, a) }
            function compAlph(a, b) { return compNodesAlphaNum(b, a) }
            function compPerf(a, b) { return compNodesPerformance(b, a) }
            break;
        case 'left':
        default:
            function compStandard(a, b) { return compNodesStandard(a, b) }
            function compAlph(a, b) { return compNodesAlphaNum(a, b) }
            function compPerf(a, b) { return compNodesPerformance(a, b) }
            break;
    }
    var root = domRoot(nodes[0]);
    switch (selected('sort')) {
        case 'standard':
            root.sort(compStandard);
            break;
        case 'alphanumeric':
            root.sort(compAlph);
            break;
        case 'performance':
            root.sort(compPerf);
            break;
    }
    return nodes;
}


// Return whether or not a node is a leaf.
function isLeaf(node) {
    return node.firstChild === null;
}


// Return whether or not a node is the root.
function isRoot(node) {
    return node.parentNode === null;
}

// Return the root of a DOM tree given any node.
function domRoot(node) {
  while (node.parentNode)
    node = node.parentNode;
  return node;
}

// Add the value of one node to another.
function addNodeValue(sumNode, addNode) {
    sumNode.nodeValue.Points += addNode.nodeValue.Points;
    sumNode.nodeValue.Possible += addNode.nodeValue.Possible;
    sumNode.nodeValue.statePoints += addNode.nodeValue.statePoints;
    sumNode.nodeValue.statePossible += addNode.nodeValue.statePossible;
    sumNode.nodeValue.itemPossible += addNode.nodeValue.itemPossible;
}

// Return a percentage points scored string for a given node.
function percentScoreString(node) {
    var p = nodePerformance(node);
    var s = '';
    if (node.nodeValue.calc == 'subtractState' && p >= 0) s = '+';
    return s + (p * 100).toFixed() + '%';
}

// Return an average points scored string for a given node.
function avePointsString(node) {
    var digits = 0 + (node.nodeValue.itemPossible < 10);
    return Number(node.nodeValue.Points / node.nodeValue.Possible * node.nodeValue.itemPossible).toFixed(digits);
}

// Return the pv.Color corresponding to a given node's percent score.
function percentScoreColor(node) {
    if (node.nodeValue.calc == 'subtractState') {
        var diff = nodePerformance(node);
        diff *= 1.6;  // use more dynamic range
        diff += 0.68;  // make 0 a comfortable intensity
        diff = Math.max(0, Math.min(diff, 1));
        var intensity = Math.floor(diff * 255);
        return pv.color('rgb(0,' + intensity + ',0)')
    } else
        return pv.color('rgb(0,' + (Math.floor(nodePerformance(node) * 255)) + ',0)')
}

// Return a foreground color that will show against a node's percentScoreColor.
function cellForegroundColor(node) {
    return (node.nodeValue.Points / node.nodeValue.Possible < 0.6) ? pv.color('#DDD') : pv.color('#111');
}

// Display item text in a new browser tab or window.
function displayItem(node) {
    var c = node.nodeValue.table.getTableProperty('c');
    var url = node.nodeValue.table.getValue(node.nodeValue.row, c.Item_Text);
    window.open(url, 'item_window');  // using the same window name each time should reuse the same window
    return this;
}
